<!doctype html>
<html lang="en">
	<head>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, initial-scale=1.0">
		<title>An agnostic AI assistant into Tiro Notes</title>
		<meta name="description" content="Tiro Notes Blog and Devlog">
		<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
		<link rel="alternate" href="/blog/feed/feed.xml" type="application/atom+xml" title="Tiro Notes Blog">
		<link rel="alternate" href="/blog/feed/feed.json" type="application/json" title="Tiro Notes Blog">
		
		<style>/**
 * okaidia theme for JavaScript, CSS and HTML
 * Loosely based on Monokai textmate theme by http://www.monokai.nl/
 * @author ocodia
 */

code[class*="language-"],
pre[class*="language-"] {
	color: #f8f8f2;
	background: none;
	text-shadow: 0 1px rgba(0, 0, 0, 0.3);
	font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
	font-size: 1em;
	text-align: left;
	white-space: pre;
	word-spacing: normal;
	word-break: normal;
	word-wrap: normal;
	line-height: 1.5;

	-moz-tab-size: 4;
	-o-tab-size: 4;
	tab-size: 4;

	-webkit-hyphens: none;
	-moz-hyphens: none;
	-ms-hyphens: none;
	hyphens: none;
}

/* Code blocks */
pre[class*="language-"] {
	padding: 1em;
	margin: .5em 0;
	overflow: auto;
	border-radius: 0.3em;
}

:not(pre) > code[class*="language-"],
pre[class*="language-"] {
	background: #272822;
}

/* Inline code */
:not(pre) > code[class*="language-"] {
	padding: .1em;
	border-radius: .3em;
	white-space: normal;
}

.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
	color: #8292a2;
}

.token.punctuation {
	color: #f8f8f2;
}

.token.namespace {
	opacity: .7;
}

.token.property,
.token.tag,
.token.constant,
.token.symbol,
.token.deleted {
	color: #f92672;
}

.token.boolean,
.token.number {
	color: #ae81ff;
}

.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
	color: #a6e22e;
}

.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string,
.token.variable {
	color: #f8f8f2;
}

.token.atrule,
.token.attr-value,
.token.function,
.token.class-name {
	color: #e6db74;
}

.token.keyword {
	color: #66d9ef;
}

.token.regex,
.token.important {
	color: #fd971f;
}

.token.important,
.token.bold {
	font-weight: bold;
}
.token.italic {
	font-style: italic;
}

.token.entity {
	cursor: help;
}
/*
 * New diff- syntax
 */

pre[class*="language-diff-"] {
	--eleventy-code-padding: 1.25em;
	padding-left: var(--eleventy-code-padding);
	padding-right: var(--eleventy-code-padding);
}
.token.deleted {
	background-color: hsl(0, 51%, 37%);
	color: inherit;
}
.token.inserted {
	background-color: hsl(126, 31%, 39%);
	color: inherit;
}

/* Make the + and - characters unselectable for copy/paste */
.token.prefix.unchanged,
.token.prefix.inserted,
.token.prefix.deleted {
	-webkit-user-select: none;
	user-select: none;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	padding-top: 2px;
	padding-bottom: 2px;
}
.token.prefix.inserted,
.token.prefix.deleted {
	width: var(--eleventy-code-padding);
	background-color: rgba(0,0,0,.2);
}

/* Optional: full-width background color */
.token.inserted:not(.prefix),
.token.deleted:not(.prefix) {
	display: block;
	margin-left: calc(-1 * var(--eleventy-code-padding));
	margin-right: calc(-1 * var(--eleventy-code-padding));
	text-decoration: none; /* override del, ins, mark defaults */
	color: inherit; /* override del, ins, mark defaults */
}
* { box-sizing: border-box; }
/* Defaults */
:root {
		--font-family:  Source Sans Pro,Helvetica Neue,Arial,sans-serif;
		--font-family-monospace: Consolas, Menlo, Monaco, Andale Mono WT, Andale Mono, Lucida Console, Lucida Sans Typewriter, DejaVu Sans Mono, Bitstream Vera Sans Mono, Liberation Mono, Nimbus Mono L, Courier New, Courier, monospace;
}

/* Theme colors */
:root {
		--color-main-red: #f56e6e;
		--color-gray-20: #e0e0e0;
		--color-gray-50: #C0C0C0;
		--color-gray-90: #333;

		--background-color: #fff;

		--text-color: var(--color-gray-90);
		--text-color-link: #f56e6e;
		--text-color-link-active: #f56e6e;
		--text-color-link-visited: #f56e6e;

		--syntax-tab-size: 2;
}

@media (prefers-color-scheme: dark) {
		:root {
				--color-gray-20: #1e1e1e;
				--color-gray-50: #1e1e1e;
				--color-gray-90: #1e1e1e;

				/* --text-color is assigned to --color-gray-_ above */
				--text-color-link:#f56e6e;
				--text-color-link-active: #f56e6e;
				--text-color-link-visited: #f56e6e;

				--background-color: #fff;
		}
}


/* Global stylesheet */
* {
		box-sizing: border-box;
}

a {
		text-decoration: none;
}

html,
body {
		padding: 0;
		margin: 0 auto;
		font-family: var(--font-family);
		color: var(--text-color);
		background-color: var(--background-color);
}
html {
		overflow-y: scroll;
}
body {
		max-width: 40em;
}

/* https://www.a11yproject.com/posts/how-to-hide-content/ */
.visually-hidden {
		clip: rect(0 0 0 0);
		clip-path: inset(50%);
		height: 1px;
		overflow: hidden;
		position: absolute;
		white-space: nowrap;
		width: 1px;
}

p:last-child {
		margin-bottom: 0;
}
p {
		line-height: 1.5;
}

li {
		line-height: 1.5;
}

a[href] {
		color: var(--text-color-link);
}
a[href]:visited {
		color: var(--text-color-link-visited);
}
a[href]:hover,
a[href]:active {
		color: var(--text-color-link-active);
}

main {
		padding: 1rem;
}
main :first-child {
		margin-top: 0;
}

header {
		/* border-bottom: 1px dashed var(--color-gray-20); */
}
header:after {
		content: "";
		display: table;
		clear: both;
}

.links-nextprev {
		list-style: none;
		padding: 1em 0;
		display: flex;
    justify-content: space-between;
}

table {
		margin: 1em 0;
}
table td,
table th {
		padding-right: 1em;
}

pre,
code {
		font-family: var(--font-family-monospace);
}
pre:not([class*="language-"]) {
		margin: .5em 0;
		line-height: 1.375; /* 22px /16 */
		-moz-tab-size: var(--syntax-tab-size);
		-o-tab-size: var(--syntax-tab-size);
		tab-size: var(--syntax-tab-size);
		-webkit-hyphens: none;
		-ms-hyphens: none;
		hyphens: none;
		direction: ltr;
		text-align: left;
		white-space: pre;
		word-spacing: normal;
		word-break: normal;
}
code {
		word-break: break-all;
}

/* Header */
header {
		display: flex;
		gap: 1em .5em;
		flex-wrap: wrap;
		align-items: center;
		padding: 1em;
		position:relative;
}
/*

#ARTICLE

*/

.post-metadata {
	margin-bottom: 20px!important;
}

p img {
	max-width: 90%;
	display:block;
}
code,
p code {
	background: #e6e6e6;
    padding: 4px;
    border-radius: 5px;
}
/*

#LIST

*/
/* TAG LIST */
.post-tags .tag:last-child .separator {
		display:none;
}
.post-tags .tag {

}
.post-tags {
		font-size: 10px;
		margin-left: 20px;
}
.post-tags-length-1 {
		display: none;
}
.tag-posts {
		display:none;
}
.home-link img {
		width: 50px;
}
.home-link {
		width: 170px;
		color: var(--color-gray-90)!important;
		display: flex;
		align-content: center;
    align-items: center;
    justify-content: space-between;

		font-size: 1em; /* 16px /16 */
		margin-right: 2em;
}
.home-link:link:not(:hover) {
		text-decoration: none;
}

/* Nav */
.nav {
		position: absolute;
		right: 0px;
		top: 30px;
		display: flex;
		padding: 0;
		margin: 0;
		list-style: none;
}
.nav-item {
		display: inline-block;
		margin-left: 22px;
		font-size: 15px;
		color: var(--color-gray-90);
}
.nav-item a[href]:not(:hover) {
		text-decoration: none;
		color: var(--color-gray-90);
}
.nav a[href][aria-current="page"] {
		text-decoration: none;
}

/* Posts list */
.postlist {
		list-style: none;
		padding: 0;
		padding-left: 1.5rem;
}
.postlist-item {
		display: flex;
		flex-wrap: wrap;
		align-items: baseline;
		counter-increment: start-from -1;
		margin-bottom: 1em;
}
.postlist-item:before {
		display: inline-block;
		pointer-events: none;
		content: "" counter(start-from, decimal-leading-zero) ". ";
		line-height: 100%;
		text-align: right;
		margin-left: -1.5rem;
}
.postlist-date,
.postlist-item:before {
		font-size: 0.8125em; /* 13px /16 */
		color: var(--color-gray-90);
}
.postlist-date {
		word-spacing: -0.5px;
}
.postlist-link {
		font-size: 1.1875em; /* 19px /16 */
		font-weight: 700;
		flex-basis: calc(100% - 1.5rem);
		padding-left: .25em;
		padding-right: .5em;
		text-decoration: none;

}
.postlist-item-active .postlist-link {
		font-weight: bold;
}

/* Tags */
.post-tag {
		display: inline-flex;
		align-items: center;
		justify-content: center;
		text-transform: capitalize;
		font-style: italic;
}
.postlist-item > .post-tag {
		align-self: center;
}

/* Tags list */
.post-metadata {
		display: inline-flex;
		flex-wrap: wrap;
		gap: .5em;
		list-style: none;
		padding: 0;
		margin: 0;
}
.post-metadata time {
		margin-right: 1em;
}

/* Direct Links / Markdown Headers */
.header-anchor {
		text-decoration: none;
		font-style: normal;
		font-size: 1em;
		margin-left: .1em;
}
a[href].header-anchor,
a[href].header-anchor:visited {
		color: transparent;
}
a[href].header-anchor:focus,
a[href].header-anchor:hover {
		text-decoration: none;
}
a[href].header-anchor:focus,
:hover > a[href].header-anchor {
		color: #aaa;
}

h2 + .header-anchor {
		font-size: 1.5em;
}

/* Responsive */
@media (max-width: 600px) {
		header ul.nav {
				top: 5px;
				position: relative;
				left: -15px;
		}
}</style>
	</head>
	<body>

		<script type="text/javascript">
			(function(window, document, dataLayerName, id) {
					window[dataLayerName]=window[dataLayerName]||[],window[dataLayerName].push({start:(new Date).getTime(),event:"stg.start"});var scripts=document.getElementsByTagName('script')[0],tags=document.createElement('script');
					function stgCreateCookie(a,b,c){var d="";if(c){var e=new Date;e.setTime(e.getTime()+24*c*60*60*1e3),d="; expires="+e.toUTCString()}document.cookie=a+"="+b+d+"; path=/"}
					var isStgDebug=(window.location.href.match("stg_debug")||document.cookie.match("stg_debug"))&&!window.location.href.match("stg_disable_debug");stgCreateCookie("stg_debug",isStgDebug?1:"",isStgDebug?14:-1);
					var qP=[];dataLayerName!=="dataLayer"&&qP.push("data_layer_name="+dataLayerName),isStgDebug&&qP.push("stg_debug");var qPString=qP.length>0?("?"+qP.join("&")):"";
					tags.async=!0,tags.src="https://tiro-notes.containers.piwik.pro/"+id+".js"+qPString,scripts.parentNode.insertBefore(tags,scripts);
					!function(a,n,i){a[n]=a[n]||{};for(var c=0;c<i.length;c++)!function(i){a[n][i]=a[n][i]||{},a[n][i].api=a[n][i].api||function(){var a=[].slice.call(arguments,0);"string"==typeof a[0]&&window[dataLayerName].push({event:n+"."+i+":"+a[0],parameters:[].slice.call(arguments,1)})}}(i[c])}(window,"ppms",["tm","cm"]);
			})(window, document, 'dataLayer', '8be1ea79-efe6-40a5-8eaa-77e0a5d04b75');
		</script>



		<a href="#skip" class="visually-hidden">Skip to main content</a>

		<header>
			<a href="/blog/" class="home-link">
				<img src="https://tiro-notes.org/favicon.png">
				Tiro Notes Blog
			</a>
			<nav>
				<h2 class="visually-hidden">Top level navigation menu</h2>
				<ul class="nav">
					<li class="nav-item"> <a href="/blog/tags/guides/"> guides </a> </li>
					<li class="nav-item"> <a href="/blog/tags/devlog/"> devlog </a> </li>
					<li class="nav-item"> <a href="/blog/tags/"> all tags </a> </li>
					<li class="nav-item">
						<a href="/blog/feed/feed.xml">
							<span class="fa fa-rss"></span>
					</a> </li>
					<li class="nav-item"> <a href="https://github.com/dotgreg/tiro">
							<span class="fa fa-github"></span>
					</a> </li>
					<li class="nav-item"> <a href="https://tiro-notes.org"> site > </a> </li>
				</ul>
				<!-- <ul class="nav"> -->
					<!-- -->
					<!-- 	<li class="nav-item"><a href="/">Home</a></li> -->
					<!-- -->
					<!-- 	<li class="nav-item"><a href="/blog/">Archive</a></li> -->
					<!-- -->
					<!-- </ul> -->
			</nav>
		</header>

		<main id="skip">
			
<h1>An agnostic AI assistant into Tiro Notes</h1>

<ul class="post-metadata">
	<li><time datetime="2023-06-11">11 June 2023</time></li>
	<li><a href="/blog/tags/devlog/" class="post-tag">devlog</a>, </li>
	<li><a href="/blog/tags/ai/" class="post-tag">AI</a>, </li>
	<li><a href="/blog/tags/text-editor/" class="post-tag">text editor</a></li>
</ul>

<p>One of the most interesting features that I wanted to implement in Tiro is the idea of having an AI assistant available on mobile and desktop.</p>
<video width="100%" controls="" autoplay="" loop="" muted="" markdown="1">
	<source src="https://github.com/dotgreg/tiro-notes/assets/2981891/dd42b4e3-f044-4c2e-a83d-3eb7fc9cf798" type="video/mp4" markdown="1">
</video>
<p><em>Final result, you can test it using the command line <code>npx tiro-notes@0.30.99.12</code></em></p>
<h2 id="user-experience" tabindex="-1">User experience <a class="header-anchor" href="#user-experience">#</a></h2>
<p>The main problem I saw with current solutions was the friction between the writing process and the AI assistance at the user experience level. I don't use AI assistants because of that friction required to use them (Opening a new tab, copy/pasting text 2 times etc.). It creates a certain level of steps under which I'd just rather use search engines or do it myself.</p>
<p>My first attempt was to add an AI assistant throught command line using the Tiro command line manager custom tag called Commander, but I quickly realized that the integration still required too much steps to be usable in real-life scenarios, especially the copy/pasting part which is really tedious on mobile. So I gave up in using AI assistance inside Tiro that way.</p>
<img width="40%" src="https://github.com/dotgreg/tiro-notes/assets/2981891/5c9cb665-7dd3-46c7-a8e0-647a88c93ea9">
<p><em>Initial AI integration attempt using the commander custom tag</em></p>
<p>My second attempt, which I will describe here, was to get as close as possible to the user experience offered by Microsoft's Copilot or Notion's AI system.</p>
<p>The goal was to reduce the number of steps needed to get an answer:</p>
<ul>
<li>Write the question in Tiro Notes</li>
<li>Select the text of the question</li>
<li>Immediately obtain an AI-generated response in the note</li>
</ul>
<p>The objective is also to respect a certain open source philosophy by offering the choice to users to use different AI engines, including open source ones in the future. The solution is designed to be easily interchangeable from the user settings.</p>
<img width="70%" src="https://github.com/dotgreg/tiro-notes/assets/2981891/b6124b31-a6ca-41ab-9e3e-bcd8d298ef8e">
<p><em>New settings options for AI assistant. The AI system used can be modified</em></p>
<h2 id="technical-implementation" tabindex="-1">Technical implementation <a class="header-anchor" href="#technical-implementation">#</a></h2>
<p>On the coding side, it happened to be quite easy to implement it.
The main logic was :</p>
<ul>
<li>User select a part of the text</li>
<li>A small popup appears suggesting AI assistance</li>
<li>If clicked, the selected text is sent to Tiro Notes backend that executes a configurable command line with it as a parameter</li>
<li>The answer is then sent back to the active note, below the question</li>
<li>As for ChatGPT, the answer is not sent once but streamed (almost word by word), I had to create a new api function <code>api.command.stream</code></li>
</ul>
<p>The code is visible at that <a href="https://github.com/dotgreg/tiro-notes/blob/4d4a845c15aa53b6e59830f4268445e9f96c09bb/client/src/components/dualView/CodeMirrorEditor.component.tsx#L386/">link</a>, I will probably refactor it latest to its own manager file</p>
<p>I am using two api functions:</p>
<ul>
<li>
<p>the newly created  <code>api.command.stream(cmd, streamChunk =&gt; { })</code> to execute the command on Tiro Notes backend. That function sends from the react client frontend a command line to the node.js backend, which executes it using the library execa. Details of the backend function used can be checked at that <a href="https://github.com/dotgreg/tiro-notes/blob/4d4a845c15aa53b6e59830f4268445e9f96c09bb/server/src/managers/exec.manager.ts">link</a></p>
</li>
<li>
<p>api.file.saveContent(p.file.path, nText) to save the note regularly</p>
</li>
</ul>
<pre class="language-js" tabindex="0"><code class="language-js"><span class="token function">getApi</span><span class="token punctuation">(</span><span class="token parameter">api</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
  <span class="token keyword">let</span> cmd <span class="token operator">=</span> api<span class="token punctuation">.</span>userSettings<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">"ui_editor_ai_command"</span><span class="token punctuation">)</span>
  cmd <span class="token operator">=</span> cmd<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token string">""</span><span class="token punctuation">,</span> selectionTxt<span class="token punctuation">)</span>
  <span class="token function">genTextAt</span><span class="token punctuation">(</span><span class="token function">genParams</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
  api<span class="token punctuation">.</span>command<span class="token punctuation">.</span><span class="token function">stream</span><span class="token punctuation">(</span>cmd<span class="token punctuation">,</span> <span class="token parameter">streamChunk</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span>streamChunk<span class="token punctuation">.</span>isError<span class="token punctuation">)</span> isError <span class="token operator">=</span> <span class="token boolean">true</span>
    <span class="token comment">// if it is an error, display it in a popup</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span>isError<span class="token punctuation">)</span> <span class="token punctuation">{</span>
      api<span class="token punctuation">.</span>ui<span class="token punctuation">.</span>notification<span class="token punctuation">.</span><span class="token function">emit</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
        <span class="token literal-property property">content</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">[AI] Error from CLI &lt;br/> "</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>cmd<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">" &lt;br/>=> &lt;br/></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>streamChunk<span class="token punctuation">.</span>text<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span>
        <span class="token literal-property property">options</span><span class="token operator">:</span> <span class="token punctuation">{</span><span class="token literal-property property">hideAfter</span><span class="token operator">:</span> <span class="token operator">-</span><span class="token number">1</span> <span class="token punctuation">}</span>
      <span class="token punctuation">}</span><span class="token punctuation">)</span>
      <span class="token function">genTextAt</span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token operator">...</span><span class="token function">genParams</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token literal-property property">textUpdate</span><span class="token operator">:</span><span class="token string">""</span><span class="token punctuation">,</span> <span class="token literal-property property">isLast</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">}</span><span class="token punctuation">)</span>
    <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span>
      <span class="token comment">// else insert it</span>
      <span class="token function">genTextAt</span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token operator">...</span><span class="token function">genParams</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token literal-property property">textUpdate</span><span class="token operator">:</span>streamChunk<span class="token punctuation">.</span>textTot<span class="token punctuation">,</span> <span class="token literal-property property">isLast</span><span class="token operator">:</span> streamChunk<span class="token punctuation">.</span>isLast<span class="token punctuation">}</span><span class="token punctuation">)</span>
    <span class="token punctuation">}</span>
  <span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre>
<p>You can test that AI assistant by downloading Tiro Notes from the command line <code>npx tiro-notes@0.30.99.12</code></p>

<ul class="links-nextprev"><li>Previous: <a href="/blog/devlog/2023/06/hello-world-devlog.html">Hello World Devlog</a></li><li>Next: <a href="/blog/devlog/2023/06/improving-productivity-omnibox-history.html">Improving productivity with the new Omnibox History</a></li>
</ul>

		</main>

		<footer></footer>

		<!-- Current page: /blog/devlog/2023/06/agnostic-ai-assistant-into-tiro-notes.html -->
	</body>
</html>
